﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Diagnostics;
using System.Threading;
using System.Transactions;
using System.Reflection;
using System.IO;

using WcfCore;
using WcfServices;

namespace WcfClientHost
{
	public class WcfPerformanceTestService : IDisposable
	{
		public WcfPerformanceTestService(WcfTestInput input)
		{
			_input = input;
			_isSession = ((input.NumberOfMessagesInSession > 0) && input.InstancingMode.Equals(InstanceContextMode.PerSession));
			_isOneWay = this.Input.CallMode.Equals(CallModes.OneWay);

			if (IsOneWay)
			{
				_oneWayLatencyTestServiceHost = new ServiceHost(typeof(WcfOneWayLatencyTestService));
				OneWayLatencyTestServiceHost.Open();
			}

			_proxy = WcfClientFactory.CreateWcfClient(input);

			string dir = Path.GetDirectoryName(WcfTestInput.TickCountOutputFilepath);
			if (!Directory.Exists(dir))
			{
				Directory.CreateDirectory(dir);
			}

			if (!File.Exists(WcfTestInput.TickCountOutputFilepath))
			{
				using (FileStream stream = new FileStream(WcfTestInput.TickCountOutputFilepath, FileMode.Create, FileAccess.Write, FileShare.Read))
				{
					using (StreamWriter writer = new StreamWriter(stream))
					{
						writer.WriteLine("Datetime,Scope,InstancingMode,Binding,CallMode,Serializer,ReceiveDataMethod,DataSize,#Messages,#MessagesInSession,Callcount,TotalMethodTickcount,AvgMethodMicroSecs,Correction,CorrectedAvgMethodMicroSecs,TotalTransactionTickCount,AvgTransactionMicroSecs,TotalSessionTickCount,AvgSessionMicroSecs");
					}
				}
			}
		}

		private readonly WcfTestInput _input;
		public WcfTestInput Input
		{
			get
			{
				return _input;
			}
		}

		private readonly WcfTestOutput _output = new WcfTestOutput();
		public WcfTestOutput Output
		{
			get
			{
				return _output;
			}
		}

		private readonly Stopwatch _transactionSw = new Stopwatch();
		public Stopwatch TransactionStopwatch
		{
			get
			{
				return _transactionSw;
			}
		}

		private readonly Stopwatch _sessionSw = new Stopwatch();
		public Stopwatch SessionStopwatch
		{
			get
			{
				return _sessionSw;
			}
		}

		private readonly ServiceHost _oneWayLatencyTestServiceHost;
		public ServiceHost OneWayLatencyTestServiceHost
		{
			get
			{
				return _oneWayLatencyTestServiceHost;
			}
		}

		private byte[] _data;
		public byte[] Data
		{
			get
			{
				return _data;
			}
			set
			{
				_data = value;
			}
		}

		private SimpleContainer _simpleContainer;
		public SimpleContainer SimpleContainer
		{
			get
			{
				return _simpleContainer;
			}
			set
			{
				_simpleContainer = value;
			}
		}

		private AdvancedContainer _advancedContainer;
		public AdvancedContainer AdvancedContainer
		{
			get
			{
				return _advancedContainer;
			}
			set
			{
				_advancedContainer = value;
			}
		}

		private Stream _dataStream;
		public Stream DataStream
		{
			get
			{
				return _dataStream;
			}
			set
			{
				_dataStream = value;
			}
		}

		private string _text;
		public string Text
		{
			get
			{
				return _text;
			}
			set
			{
				_text = value;
			}
		}

		private SimpleTextContainer _simpleTextContainer;
		public SimpleTextContainer SimpleTextContainer
		{
			get
			{
				return _simpleTextContainer;
			}
			set
			{
				_simpleTextContainer = value;
			}
		}

		private AdvancedTextContainer _advancedTextContainer;
		public AdvancedTextContainer AdvancedTextContainer
		{
			get
			{
				return _advancedTextContainer;
			}
			set
			{
				_advancedTextContainer = value;
			}
		}

		// Add data contract and xml serializer members (maybe have a function that inits all based on size?
		private void InitData(int size)
		{
			this.Data = new byte[size];
			this.Data[0] = 1;
			this.DataStream = new MemoryStream(this.Data);
			this.SimpleContainer = new SimpleContainer(this.Data);

			char[] text = new char[size];
			text[0] = 'a';
			this.Text = new string(text);

			List<AdvancedContainer> branchContainers = new List<AdvancedContainer>(3);
			List<AdvancedContainer> leafContainers = new List<AdvancedContainer>(3);
			if (WcfTestInput.UseUniqueDataInAdvancedContainer)
			{
				for (byte i = 2; i < 5; i++)
				{
					byte[] data = new byte[size];
					data[0] = i;
					AdvancedContainer leafContainer = new AdvancedContainer(data);
					leafContainers.Add(leafContainer);
				}

				for (byte j = 5; j < 8; j++)
				{
					byte[] data = new byte[size];
					data[0] = j;
					AdvancedContainer branchContainer = new AdvancedContainer(data, leafContainers.ToArray());
					branchContainers.Add(branchContainer);
				}
			}
			else
			{
				AdvancedContainer leafContainer = new AdvancedContainer(this.Data);
				leafContainers.Add(leafContainer);
				leafContainers.Add(leafContainer);
				leafContainers.Add(leafContainer);

				AdvancedContainer branchContainer = new AdvancedContainer(this.Data, leafContainers.ToArray());
				branchContainers.Add(branchContainer);
				branchContainers.Add(branchContainer);
				branchContainers.Add(branchContainer);
			}

			this.AdvancedContainer = new AdvancedContainer(this.Data, branchContainers.ToArray());

			List<AdvancedTextContainer> branchTextContainers = new List<AdvancedTextContainer>(3);
			List<AdvancedTextContainer> leafTextContainers = new List<AdvancedTextContainer>(3);
			if (WcfTestInput.UseUniqueDataInAdvancedContainer)
			{
				for (byte i = 2; i < 5; i++)
				{
					text = new char[size];
					text[0] = 'a';
					string stext = new string(text);
					AdvancedTextContainer leafTextContainer = new AdvancedTextContainer(stext);
					leafTextContainers.Add(leafTextContainer);
				}

				for (byte j = 5; j < 8; j++)
				{
					text = new char[size];
					text[0] = 'a';
					string stext = new string(text);
					AdvancedTextContainer branchTextContainer = new AdvancedTextContainer(stext, leafTextContainers.ToArray());
					branchTextContainers.Add(branchTextContainer);
				}
			}
			else
			{
				AdvancedTextContainer leafTextContainer = new AdvancedTextContainer(this.Text);
				leafTextContainers.Add(leafTextContainer);
				leafTextContainers.Add(leafTextContainer);
				leafTextContainers.Add(leafTextContainer);

				AdvancedTextContainer branchTextContainer = new AdvancedTextContainer(this.Text, leafTextContainers.ToArray());
				branchTextContainers.Add(branchTextContainer);
				branchTextContainers.Add(branchTextContainer);
				branchTextContainers.Add(branchTextContainer);
			}

			this.AdvancedTextContainer = new AdvancedTextContainer(this.Text, branchTextContainers.ToArray());
		}

		//private readonly WcfDuplexCallbackHandler _callbackHandler = new WcfDuplexCallbackHandler();
		//public WcfDuplexCallbackHandler CallbackHandler
		//{
		//    get
		//    {
		//        return _callbackHandler;
		//    }
		//}

		private byte[] _callbackReceivedData;
		public byte[] CallbackReceivedData
		{
			get
			{
				return _callbackReceivedData;
			}
			set
			{
				_callbackReceivedData = value;
			}
		}

		private long _messagesInSession;
		public long MessagesInSession
		{
			get
			{
				return _messagesInSession;
			}
			set
			{
				_messagesInSession = value;
			}
		}

		/// <summary>
		/// Can't be readonly due to PerSession need to recreate
		/// </summary>
		private IWcfServiceContract _proxy;
		public IWcfServiceContract Proxy
		{
			get
			{
				return _proxy;
			}
			set
			{
				_proxy = value;
			}
		}

		private bool _isSession;
		public bool IsSession
		{
			get
			{
				return _isSession;
			}
			set
			{
				_isSession = value;
			}
		}

		private bool _isOneWay;
		public bool IsOneWay
		{
			get
			{
				return _isOneWay;
			}
			set
			{
				_isOneWay = value;
			}
		}

		private double _oneWayLatencyMicroSecs;
		public double OneWayLatencyMicroSecs
		{
			get
			{
				return _oneWayLatencyMicroSecs;
			}
			set
			{
				_oneWayLatencyMicroSecs = value;
			}
		}

		public WcfTestOutput DoPerformanceTest()
		{
			// Measure OneWay time
			if (IsOneWay)
			{
				// First time, for JIT compilation.
				this.Proxy.OneWayLatencyTest();
				WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();

				WcfOneWayLatencyTestService.WOWLTSSW.Reset();
				for (int i = 0; i < WcfTestInput.OneWayLatencyTestCount; i++)
				{
					WcfOneWayLatencyTestService.WOWLTSSW.Start();
					this.Proxy.OneWayLatencyTest();
					WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
				}

				this.OneWayLatencyMicroSecs = WcfOneWayLatencyTestService.OneWayLatencyMicroSecs / WcfTestInput.OneWayLatencyTestCount;
			}

			foreach (ReceiveDataMethods receiveDataMethod in WcfTestInput.RequestedMethods)
			{
				foreach (int dataSize in WcfTestInput.RequestedDataSizes)
				{
					DoPerformanceTest(receiveDataMethod, dataSize);
				}
			}

			return this.Output;
		}

		private void DoPerformanceTest(ReceiveDataMethods receiveDataMethod, int dataSize)
		{
			this.Input.ReceiveDataMethod = receiveDataMethod;
			this.Input.DataSize = dataSize;

			// Transactional and exceptions is useless to test in OneWay scenarios
			if (this.Input.CallMode.Equals(CallModes.OneWay) &&
				(this.Input.ReceiveDataMethod.Equals(ReceiveDataMethods.ReceiveDataTransactional)) || (this.Input.ReceiveDataMethod.Equals(ReceiveDataMethods.ReceiveDataThrowException)))
			{
				return;
			}

			InitData((int)this.Input.DataSize);

			// Initialize transaction, only open it when transactional
			TransactionScope scope = null;
			TimeSpan transactionTimespan = new TimeSpan(1, 0, 0);

			bool transactional = this.Input.Transactional;
			if (transactional)
			{
				scope = new TransactionScope(TransactionScopeOption.Required, transactionTimespan);
			}

			this.MessagesInSession = 0;
			WcfLogger.Log(string.Format("Current Process Id: {0}; Current AppDomain Id: {1}.", Process.GetCurrentProcess().Id, AppDomain.CurrentDomain.Id));
			WcfLogger.Log(string.Format("Client calling: {0}", this.Input.DisplayName));

			this.Proxy.Initialize(this.Input);
			if (IsOneWay)
			{
				WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
			}

			// Perform 1st time to get JIT compilation out of the way.
			CallMethod();
			WcfOneWayLatencyTestService.WOWLTSSW.Reset();
			WcfOneWayLatencyTestService.WOWLTSSW.Reset();

			for (this.Output.CallCount = 0; this.Output.CallCount < this.Input.NumberOfMessages; this.Output.CallCount++)
			{
				CallMethod();

				// Keep track of #msgs in session, reset session/transaction if necessary
				this.MessagesInSession++;
				if (this.MessagesInSession >= this.Input.NumberOfMessagesInSession)
				{
					this.MessagesInSession = 0;

					// Reset transaction
					if (transactional)
					{
						TransactionStopwatch.Start();
						scope.Complete();
						scope = new TransactionScope(TransactionScopeOption.Required, transactionTimespan);
						TransactionStopwatch.Stop();
					}

					if (this.IsSession)
					{
						// Reset session; only if the channel hasn't been put in faulted state by WcfException
						if (!this.Input.ReceiveDataMethod.Equals(ReceiveDataMethods.ReceiveDataThrowException))
						{
							SessionStopwatch.Start();
							this.Proxy.TerminateSession();
							if (IsOneWay)
							{
								WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
							}

							// Recreate the proxy, because the session has been teminated.
							ICommunicationObject co = this.Proxy as ICommunicationObject;
							co.Close();

							this.Proxy = WcfClientFactory.CreateWcfClient(this.Input);
							SessionStopwatch.Stop();
						}
					}
				}

				// Wait between sending messages, if necessary
				if (WcfTestInput.WaitTimeBetweenMessagesInMSecs > 0)
				{
					Thread.Sleep(WcfTestInput.WaitTimeBetweenMessagesInMSecs);
				}
			}

			this.Output.MethodTickCount = WcfOneWayLatencyTestService.WOWLTSSW.ElapsedTicks;
			this.Output.TransactionTickCount = this.TransactionStopwatch.ElapsedTicks;
			this.Output.SessionTickCount = this.SessionStopwatch.ElapsedTicks;

			WriteTickCountToOutputFile(this.Input, this.Output);
		}

		private void CallMethod()
		{
			// Apply exception handling to catch any deliberately thrown WcfException exceptions
			try
			{
				switch (this.Input.ReceiveDataMethod)
				{
					case ReceiveDataMethods.ReceiveData:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveData(this.Data);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveSC:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveSC(this.SimpleContainer);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveAC:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveAC(this.AdvancedContainer);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveStream:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveStream(this.DataStream);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						this.DataStream.Seek(0, SeekOrigin.Begin);
						break;
					case ReceiveDataMethods.ReceiveText:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveText(this.Text);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveSTC:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveSTC(this.SimpleTextContainer);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveATC:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveATC(this.AdvancedTextContainer);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveDataThrowException:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveDataThrowException(this.Data);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
					case ReceiveDataMethods.ReceiveDataTransactional:
						WcfOneWayLatencyTestService.WOWLTSSW.Start();
						this.Proxy.ReceiveDataTransactional(this.Data);
						if (IsOneWay)
						{
							WcfOneWayLatencyTestService.ReceiptEvent.WaitOne();
						}
						WcfOneWayLatencyTestService.WOWLTSSW.Stop();
						break;
				}
			}
			catch
			{
				// Only rethrow when we are not expecting exceptions
				if (!this.Input.ReceiveDataMethod.Equals(ReceiveDataMethods.ReceiveDataThrowException))
				{
					throw;
				}
			}
		}

		private void WriteTickCountToOutputFile(WcfTestInput input, WcfTestOutput output)
		{
			double correctedMethodMicroSecs = output.AvgMethodMicroSecs - this.OneWayLatencyMicroSecs;
			string log = string.Format("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18}",
				DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss"),
				input.Scope.ToString(),
				input.InstancingMode.ToString(),
				input.Binding.ToString(),
				input.CallMode.ToString(),
				input.Serializer.ToString(),
				input.ReceiveDataMethod.ToString(),
				Convert.ToInt32(input.DataSize).ToString(),
				input.NumberOfMessages,
				input.NumberOfMessagesInSession,
				output.CallCount,
				output.MethodTickCount,
				output.AvgMethodMicroSecs,
				this.OneWayLatencyMicroSecs,
				correctedMethodMicroSecs,
				output.TransactionTickCount,
				output.AvgTransactionMicroSecs,
				output.SessionTickCount,
				output.AvgSessionMicroSecs);

			using (FileStream stream = new FileStream(WcfTestInput.TickCountOutputFilepath, FileMode.Append, FileAccess.Write, FileShare.Read))
			{
				using (StreamWriter writer = new StreamWriter(stream))
				{
					writer.WriteLine(log);
				}
			}
		}

		#region IDisposable Members
		private bool _disposed;
		private void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}

			if (disposing)
			{
				// Cleanup managed
				if (OneWayLatencyTestServiceHost != null)
				{
					OneWayLatencyTestServiceHost.Close();
				}
			}

			// Cleanup unmanaged

			_disposed = true;
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		~WcfPerformanceTestService()
		{
			Dispose(false);
		}
		#endregion
	}
}
